package jgroups.demos;

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Menu;
import java.awt.MenuBar;
import java.awt.MenuItem;
import java.awt.Panel;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.nio.ByteBuffer;

import org.jgroups.Channel;
import org.jgroups.Global;
import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.ReceiverAdapter;
import org.jgroups.View;
import org.jgroups.util.Util;

/**
 * Originally written to be a demo for TOTAL order (code to be written by a student). In the meantime,
 * it evolved into a state transfer demo. All members maintain a shared matrix and continually
 * broadcast changes to be applied to a randomly chosen field (e.g. multiplication of field with new
 * value, division, addition, subtraction). Each member can be started independently (starts to
 * broadcast update messages to all members). When "Stop" is pressed, a stop message is broadcast to
 * all members, causing them to stop sending messages. The "Clear" button clears the shared state;
 * "GetState" refreshes it from the shared group state (using the state transfer protocol).<p>If the
 * demo is to be used to show total order, then the SEQUENCER protocol would have to be added to the
 * stack.
 *
 * @author Bela Ban
 */
public class TotalOrder extends Frame {
	final Font def_font = new Font("Helvetica", Font.BOLD, 12);
	final Font def_font2 = new Font("Helvetica", Font.PLAIN, 12);
	MyCanvas canvas;
	final MenuBar menubar = createMenuBar();
	final Button start = new Button("Start");
	final Button stop = new Button("Stop");
	final Button clear = new Button("Clear");
	final Button get_state = new Button("Get State");
	final Button quit = new Button("Quit");
	final Panel button_panel = new Panel();
	SenderThread sender = null;
	Channel channel;
	long timeout = 0;
	int field_size = 0;
	int num_fields = 0;
	static final int x_offset = 30;
	static final int y_offset = 40;
	private int num = 0;

	private int num_additions = 0, num_subtractions = 0, num_divisions = 0,
			num_multiplications = 0;

	void error(String s) {
		System.err.println(s);
	}

	class EventHandler extends WindowAdapter {
		final Frame gui;

		public EventHandler(Frame g) {
			gui = g;
		}

		public void windowClosing(WindowEvent e) {
			gui.dispose();
			System.exit(0);
		}
	}

	class SenderThread extends Thread {
		TotOrderRequest req;
		boolean running = true;

		public void stopSender() {
			running = false;
			interrupt();
			System.out.println("-- num_additions: " + num_additions
					+ "\n-- num_subtractions: " + num_subtractions
					+ "\n-- num_divisions: " + num_divisions
					+ "\n-- num_multiplications: " + num_multiplications);
			num_additions = num_subtractions = num_multiplications = num_divisions = 0;
		}

		public void run() {
			this.setName("SenderThread");

			byte[] buf;
			int cnt = 0;
			while (running) {
				try {
					req = createRandomRequest();
					buf = req.toBuffer();
					channel.send(new Message(null, null, buf));
					System.out.print("-- num requests sent: " + cnt + "\r");
					if (timeout > 0)
						Util.sleep(timeout);
					cnt++;
					if (num > 0 && cnt > num) {
						running = false;
						cnt = 0;
					}
				} catch (Exception e) {
					error(e.toString());
					return;
				}
			}
		}
	}

	void processRequest(TotOrderRequest req) throws Exception {
		int x = req.x, y = req.y, val = req.val;

		if (req.type == TotOrderRequest.STOP) {
			stopSender();
			return;
		}

		switch (req.type) {
		case TotOrderRequest.ADDITION:
			canvas.addValueTo(x, y, val);
			num_additions++;
			break;
		case TotOrderRequest.SUBTRACTION:
			canvas.subtractValueFrom(x, y, val);
			num_subtractions++;
			break;
		case TotOrderRequest.MULTIPLICATION:
			canvas.multiplyValueWith(x, y, val);
			num_multiplications++;
			break;
		case TotOrderRequest.DIVISION:
			canvas.divideValueBy(x, y, val);
			num_divisions++;
			break;
		}
		canvas.update();
	}

	public TotalOrder(String title, long timeout, int num_fields,
			int field_size, String props, int num) {
		Dimension s;

		this.timeout = timeout;
		this.num_fields = num_fields;
		this.field_size = field_size;
		this.num = num;
		setFont(def_font);

		start.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				startSender();
			}
		});

		stop.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
					TotOrderRequest req = new TotOrderRequest(
							TotOrderRequest.STOP, 0, 0, 0);
					byte[] buf = req.toBuffer();
					channel.send(new Message(null, null, buf));
				} catch (Exception ex) {
				}
			}
		});

		clear.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				canvas.clear();
			}
		});

		get_state.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
					channel.getState(null, 3000);
				} catch (Throwable t) {
					error("exception fetching state: " + t);
				}
			}
		});

		quit.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				channel.disconnect();
				channel.close();
				System.exit(0);
			}
		});

		setTitle(title);
		addWindowListener(new EventHandler(this));
		setBackground(Color.white);
		setMenuBar(menubar);

		setLayout(new BorderLayout());
		canvas = new MyCanvas(num_fields, field_size, x_offset, y_offset);

		add("Center", canvas);
		button_panel.setLayout(new FlowLayout());
		button_panel.setFont(def_font2);
		button_panel.add(start);
		button_panel.add(stop);
		button_panel.add(clear);
		button_panel.add(get_state);
		button_panel.add(quit);
		add("South", button_panel);

		s = canvas.getSize();
		s.height += 100;
		setSize(s);

		try {
			channel = new JChannel(props);
			channel.setReceiver(new ReceiverAdapter() {
				public void receive(Message msg) {
					try {
						TotOrderRequest req = new TotOrderRequest();
						ByteBuffer buf = ByteBuffer.wrap(msg.getBuffer());
						req.init(buf);
						processRequest(req);
					} catch (Exception e) {
						System.err.println(e);
					}
				}

				public void getState(OutputStream output) throws Exception {
					int[][] copy_of_state = canvas.getCopyOfState();
					Util.objectToStream(copy_of_state, new DataOutputStream(
							output));
				}

				public void setState(InputStream input) throws Exception {
					canvas.setState(Util.objectFromStream(new DataInputStream(
							input)));
				}

				public void viewAccepted(View view) {
					System.out.println("view = " + view);
				}
			});
			channel.connect("TotalOrderGroup");
			channel.getState(null, 8000);
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}
	}

	void startSender() {
		if (sender == null || !sender.isAlive()) {
			sender = new SenderThread();
			sender.start();
		}
	}

	void stopSender() {
		if (sender != null) {
			sender.stopSender();
			sender = null;
		}
	}

	private MenuBar createMenuBar() {
		MenuBar ret = new MenuBar();
		Menu file = new Menu("File");
		MenuItem quitm = new MenuItem("Quit");

		ret.setFont(def_font2);
		ret.add(file);

		file.addSeparator();
		file.add(quitm);

		quitm.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				System.exit(1);
			}
		});
		return ret;
	}

	private TotOrderRequest createRandomRequest() {
		TotOrderRequest ret = null;
		byte op_type = (byte) (((Math.random() * 10) % 4) + 1); // 1 - 4
		int x = (int) ((Math.random() * num_fields * 2) % num_fields);
		int y = (int) ((Math.random() * num_fields * 2) % num_fields);
		int val = (int) ((Math.random() * num_fields * 200) % 10);

		ret = new TotOrderRequest(op_type, x, y, val);
		return ret;
	}

	public static void main(String[] args) {
		TotalOrder g;
		String arg;
		long timeout = 200;
		int num_fields = 3;
		int field_size = 80;
		String props = null;
		int num = 0;

		props = "udp.xml";

		for (int i = 0; i < args.length; i++) {
			arg = args[i];
			if ("-timeout".equals(arg)) {
				timeout = Long.parseLong(args[++i]);
				continue;
			}
			if ("-num_fields".equals(arg)) {
				num_fields = Integer.parseInt(args[++i]);
				continue;
			}
			if ("-field_size".equals(arg)) {
				field_size = Integer.parseInt(args[++i]);
				continue;
			}
			if ("-help".equals(arg)) {
				System.out
						.println("\nTotalOrder [-timeout <value>] [-num_fields <value>] "
								+ "[-field_size <value>] [-props <properties (can be URL)>] [-num <num requests>]\n");
				return;
			}
			if ("-props".equals(arg)) {
				props = args[++i];
				continue;
			}
			if ("-num".equals(arg)) {
				num = Integer.parseInt(args[++i]);
			}
		}

		try {
			g = new TotalOrder("Total Order Demo on "
					+ InetAddress.getLocalHost().getHostName(), timeout,
					num_fields, field_size, props, num);
			g.setVisible(true);
		} catch (Exception e) {
			System.err.println(e);
		}
	}

}

class TotOrderRequest {
	public static final byte STOP = 0;
	public static final byte ADDITION = 1;
	public static final byte SUBTRACTION = 2;
	public static final byte MULTIPLICATION = 3;
	public static final byte DIVISION = 4;
	final static int SIZE = Global.BYTE_SIZE + Global.INT_SIZE * 3;

	public byte type = ADDITION;
	public int x = 0;
	public int y = 0;
	public int val = 0;

	public TotOrderRequest() {
	}

	TotOrderRequest(byte type, int x, int y, int val) {
		this.type = type;
		this.x = x;
		this.y = y;
		this.val = val;
	}

	public String printType() {
		switch (type) {
		case STOP:
			return "STOP";
		case ADDITION:
			return "ADDITION";
		case SUBTRACTION:
			return "SUBTRACTION";
		case MULTIPLICATION:
			return "MULTIPLICATION";
		case DIVISION:
			return "DIVISION";
		default:
			return "<unknown>";
		}
	}

	//    public void writeExternal(ObjectOutput out) throws IOException {
	//        out.writeByte(type);
	//        out.writeInt(x);
	//        out.writeInt(y);
	//        out.writeInt(val);
	//    }
	//
	//    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
	//        type=in.readByte();
	//        x=in.readInt();
	//        y=in.readInt();
	//        val=in.readInt();
	//    }

	public byte[] toBuffer() {
		ByteBuffer buf = ByteBuffer.allocate(SIZE);
		buf.put(type);
		buf.putInt(x);
		buf.putInt(y);
		buf.putInt(val);
		return buf.array();
	}

	public void init(ByteBuffer buf) {
		type = buf.get();
		x = buf.getInt();
		y = buf.getInt();
		val = buf.getInt();
	}

	public String toString() {
		return "[" + x + ',' + y + ": " + printType() + '(' + val + ")]";
	}
}

class MyCanvas extends Canvas {
	int field_size = 100;
	int num_fields = 4;
	int x_offset = 30;
	int y_offset = 30;

	final Font def_font = new Font("Helvetica", Font.BOLD, 14);
	int[][] array = null; // state

	Dimension off_dimension = null;
	Image off_image = null;
	Graphics off_graphics = null;
	final Font def_font2 = new Font("Helvetica", Font.PLAIN, 12);
	static final Color checksum_col = Color.blue;
	int checksum = 0;

	public MyCanvas(int num_fields, int field_size, int x_offset, int y_offset) {
		this.num_fields = num_fields;
		this.field_size = field_size;
		this.x_offset = x_offset;
		this.y_offset = y_offset;

		array = new int[num_fields][num_fields];
		setBackground(Color.white);
		setSize(2 * x_offset + num_fields * field_size + 30, y_offset
				+ num_fields * field_size + 50);

		for (int i = 0; i < num_fields; i++)
			for (int j = 0; j < num_fields; j++)
				array[i][j] = 0;
	}

	public void setFieldSize(int fs) {
		field_size = fs;
	}

	public void setNumFields(int nf) {
		num_fields = nf;
	}

	public void setXOffset(int o) {
		x_offset = o;
	}

	public void setYOffset(int o) {
		y_offset = o;
	}

	public void addValueTo(int x, int y, int value) {
		synchronized (array) {
			array[x][y] += value;
			repaint();
		}
	}

	public void subtractValueFrom(int x, int y, int value) {
		synchronized (array) {
			array[x][y] -= value;
			repaint();
		}
	}

	public void multiplyValueWith(int x, int y, int value) {
		synchronized (array) {
			array[x][y] *= value;
			repaint();
		}
	}

	public void divideValueBy(int x, int y, int value) {
		if (value == 0)
			return;
		synchronized (array) {
			array[x][y] /= value;
			repaint();
		}
	}

	public void setValueAt(int x, int y, int value) {
		synchronized (array) {
			array[x][y] = value;
		}
		repaint();
	}

	public int getValueAt(int x, int y) {
		synchronized (array) {
			return array[x][y];
		}
	}

	public void clear() {
		synchronized (array) {
			for (int i = 0; i < num_fields; i++)
				for (int j = 0; j < num_fields; j++)
					array[i][j] = 0;
			checksum = checksum();
			repaint();
		}
	}

	public int[][] getState() {
		synchronized (array) {
			return array;
		}
	}

	public int[][] getCopyOfState() {
		int[][] retval = new int[num_fields][num_fields];

		synchronized (array) {
			for (int i = 0; i < num_fields; i++)
				System.arraycopy(array[i], 0, retval[i], 0, num_fields);
			return retval;
		}
	}

	public void update() {
		checksum = checksum();
		repaint();
	}

	public void setState(Object new_state) {

		if (new_state == null)
			return;

		try {
			int[][] new_array = (int[][]) new_state;
			synchronized (array) {
				clear();

				for (int i = 0; i < num_fields; i++)
					System.arraycopy(new_array[i], 0, array[i], 0, num_fields);
				checksum = checksum();
				repaint();
			}
		} catch (Exception e) {
			System.err.println(e);
		}
	}

	public int checksum() {
		int retval = 0;

		synchronized (array) {
			for (int i = 0; i < num_fields; i++)
				for (int j = 0; j < num_fields; j++)
					retval += array[i][j];
		}
		return retval;
	}

	public void update(Graphics g) {
		Dimension d = getSize();

		if (off_graphics == null || d.width != off_dimension.width
				|| d.height != off_dimension.height) {
			off_dimension = d;
			off_image = createImage(d.width, d.height);
			off_graphics = off_image.getGraphics();
		}

		//Erase the previous image.
		off_graphics.setColor(getBackground());
		off_graphics.fillRect(0, 0, d.width, d.height);
		off_graphics.setColor(Color.black);
		off_graphics.setFont(def_font);
		drawEmptyBoard(off_graphics);
		drawNumbers(off_graphics);
		g.drawImage(off_image, 0, 0, this);
	}

	public void paint(Graphics g) {
		update(g);
	}

	/**
	 * Draws the empty board, no pieces on it yet, just grid lines
	 */
	void drawEmptyBoard(Graphics g) {
		int x = x_offset, y = y_offset;
		Color old_col = g.getColor();

		g.setFont(def_font2);
		old_col = g.getColor();
		g.setColor(checksum_col);
		g.drawString(("Checksum: " + checksum), x_offset + field_size,
				y_offset - 20);
		g.setFont(def_font);
		g.setColor(old_col);

		for (int i = 0; i < num_fields; i++) {
			for (int j = 0; j < num_fields; j++) { // draws 1 row
				g.drawRect(x, y, field_size, field_size);
				x += field_size;
			}
			g.drawString((String.valueOf((num_fields - i - 1))), x + 20, y
					+ field_size / 2);
			y += field_size;
			x = x_offset;
		}

		for (int i = 0; i < num_fields; i++) {
			g.drawString((String.valueOf(i)), x_offset + i * field_size
					+ field_size / 2, y + 30);
		}
	}

	void drawNumbers(Graphics g) {
		Point p;
		String num;
		FontMetrics fm = g.getFontMetrics();
		int len = 0;

		synchronized (array) {
			for (int i = 0; i < num_fields; i++)
				for (int j = 0; j < num_fields; j++) {
					num = String.valueOf(array[i][j]);
					len = fm.stringWidth(num);
					p = index2Coord(i, j);
					g.drawString(num, p.x - (len / 2), p.y);
				}
		}
	}

	Point coord2Index(int x, int y) {
		Point ret = new Point();

		ret.x = x_offset + (x * field_size);
		ret.y = y_offset + ((num_fields - 1 - y) * field_size);
		return ret;
	}

	Point index2Coord(int i, int j) {
		int x = x_offset + i * field_size + field_size / 2;

		// int y=y_offset + j*field_size + field_size/2;

		int y = y_offset + num_fields * field_size - j * field_size
				- field_size / 2;

		return new Point(x, y);
	}

}
